<?php
class protocol_unreal extends protocol_module {
	private $password;
	protected $numeric;
	public $binds = array();
	public function connect($host, $port, $password, $parameters = array()) {
		if ($this->connection) return true;
		if (!$password) return false;
		if (!isset($parameters['timeout']))
			$parameters['timeout'] = 2;
		if (!isset($parameters['retry']))
			$parameters['retry'] = 5;
		$this->password = $password;
		unset($password);
		$this->numeric =& $this->me->numeric;
		$this->state = 1;
		$errcatch = 0;
		$errstrcatch = '';
		$this->connection = @fsockopen($host,$port,$errcatch,$errstrcatch,$parameters['timeout']);
		if (!$this->connection) {
			throw new Exception("Failed to establish a connection: $errcatch $errstrcatch",022);
			//todo retry
			$this->state = 0;
			return false;
		}
		$this->status = 2;
		error_log('UnrealIRCd connection established, syncing up...',E_USER_NOTICE);
		$this->send("PROTOCTL NOQUIT TOKEN SJOIN SJOIN2 NICKv2 UMODE2 VL SJ3 NS TKLEXT ".
		"SJB64 NICKIP VHP CHANMODES=beIqa,kfL,lj,psmntirRcOAQKVGCuzNSMTG NICKCHARS=\r\n".
		"PASS :".$this->password."\r\nSERVER ".$this->name." 0 :U*-*-".$this->me->numeric." ".version);
		return true;
	}
	public function disconnect() {
		$this->state = 0;
		if (!$this->connection) return true;
		$this->send_native('error',':bye bye');
		fclose($this->connection);
		return true;
	}
	public function send($input) {
		return fwrite ($this->connection,$input."\r\n");
	}
	public function receive() {
		if (!$this->connection)
		{
            throw new exception_unreal_connectiondied("connection died");
		}
		stream_set_timeout($this->connection,1,0);
		$res = fgets($this->connection, 10240);
		$info = stream_get_meta_data($this->connection);
		if ($info['eof']) {
			throw new Exception("Connection terminated by remote host",023);
		}
		if ($info['timed_out'])
			return true;
		$this->parser(substr($res,0,-2));
//		print("<code>".substr($res,0,-2)."</code><br />\n");
		$info = stream_get_meta_data($this->connection);
		return (!$info['unread_bytes']);
	}
	public function parser($buffer) {
		$GLOBALS['__DEBUG_LASTPARSE'] = $buffer;
		$command = '';
		$source = null;
		if (!empty($this->me->leafs)) {
//			reset($this->me->leafs);
//			$myleaf =& $this->me->leafs[key($this->me->leafs)];
			$myleaf =& firstarr($this->me->leafs);
		} else {
			$myleaf =& $this->me; //workaround for first server
		}
		if ($buffer{0} == ':' or $buffer{0} == '@') {
			$par = explode(' ',$buffer,3);
			list($source, $command) = $par;
			$par = (count($par) == 3 ? $par[2] : null);
			if ($source{0} == ':') {
				$source = substr($source,1);
				if (array_key_exists($source,$GLOBALS['servers'])) {
					$source =& $GLOBALS['servers'][$source];
				} elseif (array_key_exists(strtolower($source),$GLOBALS['users'])) {
					$source =& $GLOBALS['users'][strtolower($source)];
				} else {
					error_log("invalid server name $source, object could not be found");
					$source =& $myleaf;
				}
			}
			else {
				$source = (int)substr($source,1);
				if (array_key_exists($source,$GLOBALS['servers_by_numeric'])) {
					$source =& $GLOBALS['servers_by_numeric'][$source];
				} else {
					error_log("invalid server numeric $source, object could not be found");
					$source =& $myleaf;
				}
			}
		} else {
			$source =& $myleaf;
			$exp = explode(' ',$buffer,2);
			$command = $exp[0];
			$par = (isset($exp[1]) ? $exp[1] : null);
		}
		$command = $this->detokenizer($command);
		$call = (array_key_exists($command,$this->binds)
			? $this->binds[$command]
			: array(&$this, (is_numeric($command{0}) ? '_' : '')
			. strtolower($command).'_rawparser'));
		$ret = (!is_callable($call) 
			? "Unknown function for command << $source->name << $par"
			: (string) call_user_func_array($call,array(&$this, $source, $par))
		);
		if ($ret)
			error_log("PROTOCOL:$command $ret",E_USER_WARNING); //\n\t($buffer)
	}
	public function send_native($command, $parameters = null, $source = null) {
		$ret = '';
		if ($source) {
			if (!is_object($source) and !is_string($source))
				throw new Exception("bad source type",030);
			if (is_string($source)) {
				if (strpos($source,' ')!==false) {
					error_log("source has spaces (cmd: $command; parameters: $parameters; src: $source)",E_USER_WARNING);
					throw new Exception("source has spaces (cmd: $command; parameters: $parameters; src: $source)");
				}
				$ret .= ':'.$source.' ';
//			} elseif ($source instanceof server and $source->numeric) {
//				$ret .= '@'.$source->numeric.' ';
			} elseif ($source instanceof basic) {
				$ret .= ':'.$source->name.' ';
			}
		}
		$ret .= $this->tokenizer(strtoupper($command));
		if ($parameters) {
			if (strpos($parameters,"\n")!==false)
				throw new Exception("parameters have newlines!",031);
			$ret .= " $parameters";
		}
		return $this->send($ret);
	}
	protected function ping_rawparser($__unused, basic &$source, $parameters) {
		$this->send_native('pong',$parameters,$this->me);
	}
	protected function server_rawparser($__unused, basic &$source, $parameters) {
		if (empty($this->me->leafs)) {
			list($name,$hops,$tok,$desc) = explode(' ',$parameters,4);
			list(,,$numeric) = explode('-',$tok);
		} else {
			list($name,$hops,$numeric,$desc) = explode(' ',$parameters,4);
			$desc = substr($desc,1);
		}
		$hub =& $source;
		$numeric = (int)$numeric;
		$server = new server($name, compact('numeric','hub'));
		callback_module::run('server_signon',array(&$server));
	}
	protected function privmsg_rawparser($__unused, basic &$source, $parameters) {
		$target = firstword($parameters, $parameters);
		$this->make_basic($target);
		if (strpos($parameters,':') !== false)
			list(,$parameters) = explode(':',$parameters,2);
		if ($target instanceof user) {
			$this->user_message_parser($source, $target, $parameters, 'privmsg');
		} elseif ($target instanceof channel) {
			$this->channel_message_parser($source, $target, $parameters, 'privmsg');
		} else {
			return "unexpected type of target";
		}
	}
	protected function notice_rawparser($__unused, basic &$source, $parameters) {
		$target = firstword($parameters, $parameters);
		if (strpos($parameters,':') !== false)
			list(,$parameters) = explode(':',$parameters,2);
		$this->make_basic($target);
		
		//target might be null for server messages
		if ($target == null) $target =& $this->me;
		
//		if ($target instanceof server) return; //discard?
		
		$this->message_parser($source, $target, $parameters, 'notice');
	}
	protected function kick_rawparser($__unused, basic &$source, $parameters) {
		//roev << KICK << #red-board BleedingMoon :roev
		//source COMMAND target_channel target_user :reason
		$c = firstword($parameters, $parameters);
		$u = firstword($parameters, $parameters);
		if (strpos($parameters,':') !== false)
			list(,$parameters) = explode(':',$parameters,2);
		$this->make_basic($c);
		$this->make_basic($u);
		if ($c instanceof channel and $u instanceof user) {
			$this->channel_message_parser($source, $c, array(&$u, $parameters), 'kick');
			$c->kick($u, $source, $parameters);
			
		} else
			return "invalid target specified";
	}
	protected function sjoin_rawparser($__unused, basic &$source, $parameters) {
		//echo.nix.co.il << SJOIN << !13fNFp #red-board :BleedingMoon
		// << SJOIN << !150cl4 #YBB +ntfj [2c#C5,10j#R10,25m#M10,4n#N10]:5 1:60 :X squall RiP
		if (strpos($parameters,' :') === false) return "unexpeced number of parameters specified";
		list($parameters, $nicklist) = explode(' :',$parameters,2);
		list($created, $target, $modes) = explode(' ',$parameters.' ',3);
		$created = ($created{0} == '!' ? $this->to_int(substr($created,1)) : (int)$created);
		$target_new = $target;
		$this->make_basic($target);
		if (!is_object($target) or !$target instanceof channel) {
			$target = new channel($target_new, compact('created'));
			callback_module::run('channel_spawn',array(&$target, $parameters));
		} else {
			if ($target->created < $created) {
				//TODO: apply nethack, do not apply modes, send reverse sjoin
			} elseif ($modes) {
				$target->mode($source, $modes);
				//TODO: send back modes* (string)$target->modes
			}
		}
		$modesconvert = array('&' => 'b', '"' => 'e', "'" => 'I');
		foreach(explode(' ',trim($nicklist)) as $item) {
			if (!$item) continue;
			if (array_key_exists($item{0}, $modesconvert)) {
				$target->mode($source, $modesconvert[$item{0}]." ".substr($item,1));
				continue;
			}
			if (!preg_match('/^([*~@%+]*)([[A-}][A-}0-9\-]*)$/i',$item,$match)) {
				error_log("skipping bad item in sjoin list $item",E_USER_ERROR);
				continue;
			}
			list(,$modes,$nick) = $match;
			unset($match);
			$modes = str_replace(array('*','~','@','%','+'),array('q','a','o','h','v'),$modes);
			$this->make_basic($nick);
			if (!$nick instanceof user) throw new Exception("failed to convert $nick to user");
			$target->join($nick, $modes);
			$this->channel_message_parser($source, $target, array(&$nick, $modes),'join');
			unset($nick);
		}
	}
	protected function join_rawparser($__unused, basic &$source, $parameters) {
		if ($parameters != '0') return "unknown parameters: $parameters";
		if (!$source instanceof user) return "unknown source type, expected user";
		foreach ($source->channels as &$c) {
			$c->part($source);
			$this->channel_message_parser($source, $c, '', 'part');
			//todo: part from all channels reason
		}
	}
	protected function part_rawparser($__unused, basic &$source, $parameters) {
		//Mustang << PART << #c++
		$target = firstword($parameters, $parameters);
		if (strpos($parameters,':') !== false)
			list(,$parameters) = explode(':',$parameters,2);
		$this->make_basic($target);
		if ($target instanceof channel) {
			$target->part($source, $parameters);
			$this->channel_message_parser($source, $target, $parameters, 'part');
		} else
			return "unexpected target type";
	}
	protected function mode_rawparser($__unused, basic &$source, $parameters) {
		//echo.nix.co.il << MODE << #carom3d +ntfj [2c#C5,10j#R10,25m#M10,4n#N10]:5 1:60 1157995904
		$target = firstword($parameters, $parameters);
		$this->make_basic($target);
		if ($source instanceof server) {
			$tspos = strrpos($parameters, ' ');
			$ts = (int)substr($parameters, $tspos + 1);
			$parameters = substr($parameters, 0, $tspos);
			unset($tspos);
		}
		if ($target instanceof channel) {
			$target->mode($source, $parameters);
			$this->channel_message_parser($source, $target, $parameters, 'mode_chan');
		} else
			return "unexpected target type";
	}
	protected function topic_rawparser($__unused, basic &$source, $parameters) {
		//bad << TOPIC << #geto bad 1158001188 :��� ������ :(
		list($target, $topic_by, $topic_at, $topic_is) = explode(' ',$parameters,4);
		$this->make_basic($target);
		if (!$target instanceof channel) return "unexpected basic type";
		$topic_is = (string)substr($topic_is,1);
		$topic_at = ($topic_at{0} == '!' ? $this->to_int($topic_at)
			: (int)$topic_at);
		$target->topic($topic_is, $topic_by, $topic_at);
		$this->channel_message_parser($source, $target, $parameters, 'topic_chan');
	}
	protected function umode2_rawparser($__unused, basic &$source, $parameters) {
		//Sasok << UMODE2 << -R+V
		if (!$source instanceof user)
			throw exception_unreal_umode2_badsource("umode2 expected source type of user");
		modes_apply($source->modes, $parameters, true);
	}
	protected function nick_rawparser($__unused, basic &$source, $parameters) {
		if ($source instanceof server) {
			// << NICK << }vFxRDXcidbIY 1 !151R8d ~Admin bzq-25-119-252.cust.bezeqint.net 1 0 +iwRx NiX-148E4D30.cust.bezeqint.net 1Bl3/A== :taki|FxRDXcidbIY|ToMeR
			//:<sender> NICK <nickname> <hops> <TS> <username> <host> <server> <servicestamp> <umodes> <vhost> <ip> :<info>
			list($parameters, $gekos) = explode(' :',$parameters, 2);
			$parameters = explode(' ',$parameters.'     ');
			$name = $parameters[0];
			$server = $parameters[5];
			if (false===strpos($server,'.')
			and array_key_exists(hexdec($server),$GLOBALS['servers_by_numeric'])) {
				$server =& $GLOBALS['servers_by_numeric'][hexdec($server)];
			} else {
				$this->make_basic($server);
			}
			if (!$server instanceof server) throw new Exception("invalid server object");
			$u = new user($name, compact('server'));
			if (false!==strpos($parameters[7],'s'))
				throw new Exception("unhandled situation with snomasks ".implode(' ',$parameters));
			list(,,$u->signon,$u->username,$u->host,,$u->stamp,,$u->fakehost) = $parameters;
			$u->gekos = $gekos;
			modes_apply($u->modes, $parameters[7], true);
			if ($parameters[9] != '*') {
				$ip = base64_decode($parameters[9]);
				$ip = implode('.',array(ord($ip{0}),ord($ip{1}),ord($ip{2}),ord($ip{3})));
				if ($ip) $u->ip = $ip;
			}
			callback_module::run('user_signon',array(&$this, &$u));
		} elseif ($source instanceof user) {
			//}v8JBlhSSpSaI << NICK << }35p^xi^o_4u8 1158001189
			//$parameters = 'newnick timestamp'
			$target = firstword($parameters, $parameters);
			$old = array($source->name, $source->signon);
			if (!$source->nick_change($target))
				return "failed to $source->name to $target";
			if ((int)$parameters)
				$source->signon = (int)$parameters;
//			foreach ($this->channels as &$c) {
//				$this->user_message_parser($source, $c, $old, 'nick_change');
//			}
//			$this->user_message_parser($source, $this->me, $old, 'nick');
		}
	}
	protected function squit_rawparser($__unused, basic &$source, $parameters) {
		$dropservlist = array();
		list($target, $reason) = explode (' :',$parameters,2);
		$this->make_basic($target);
		$reason = $source->name . " " . $target->name;
		$target->all_leafs($dropservlist);
		foreach ($dropservlist as &$server) {
			$server->drop_users($reason);
		}
		//todo: add callback
	}
	protected function quit_rawparser($__unused, basic &$source, $parameters) {
		//}vHxbMrCezxqE << QUIT << :Client exited
		if (!$source instanceof user) return "unexpected source type";
		$source->destruct($parameters);
	}
	
	public function user_message_parser(basic &$source, user &$target, $parameters, $type) {
		if ($target->server === $this->me) {
			if (!method_exists($target, $type.'_parser'))
//				throw new Exception("Didn't find private message parser for user object.",025);
				error_log("local user $target->name doesn't have $type parser",E_USER_NOTICE);
			else
				call_user_func_array(array(&$target,$type.'_parser'),array($source, $parameters));
		} else {
			//throw new Exception("Forwarding messages not implemented",028);
		}
	}
	public function channel_message_parser(basic &$source, channel &$target, $parameters, $type) {
		foreach ($target->users as &$resident) {
			if ($resident->server === $this->me) {
				if (!method_exists($resident, $type.'_parser'))
//					throw new Exception("Didn't find private message parser for user object.",026);
					error_log("local user $resident->name doesn't have $type parser",E_USER_NOTICE);
				else
					call_user_func_array(array(&$resident,$type.'_parser'),array($source, $target, $parameters));
			}
		}
	}
	public function server_message_parser(basic &$source, server &$target, $parameters, $type) {
		//, basic &$spoof = null//($spoof ? $spoof : $target)//
		if ($target === $this->me) {
			foreach ($target->users as &$resident) {
				if (!method_exists($resident, $type.'_parser'))
//					throw new Exception("Didn't find $type message parser for user object.",027);
					error_log("local user $resident->name doesn't have $type parser",E_USER_NOTICE);
				else 
					call_user_func_array(array(&$resident,$type.'_parser'),array($source, $target, $parameters));
			}
		}
	}
	static public function to_base64($int) {
		$int = abs($int);
		$base = 64;
		$index = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}";
		$out = "";
		for ( $t = floor( log10( $int ) / log10( $base ) ); $t >= 0; $t-- ) {
			$a = floor( $int / pow( $base, $t ) );
			$out = $out . substr( $index, $a, 1 );
			$int = $int - ( $a * pow( $base, $t ) );
   		}
		return $out;
	}
	static public function to_int($base64) {
		$base = 64;
		$index = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}";
		$out = "";
		$len = strlen( $base64 ) - 1;
		for ( $t = 0; $t <= $len; $t++ ) {
			$out = $out + strpos( $index, substr( $base64, $t, 1 ) )
				* pow( $base, $len - $t );
		}
		return $out;
	}
	static public function detokenizer($token) {
		$det = array ('!' => 'PRIVMSG', '#' => 'WHOIS', '&' => 'NICK', "'" => 'SERVER',
			')' => 'TOPIC', '*' => 'INVITE', '+' => 'VERSION', ',' => 'QUIT', '-' => 'SQUIT',
			'.' => 'KILL', '/' => 'INFO', '0' => 'LINKS', '2' => 'STATS', '4' => 'HELP',
			'5' => 'ERROR', '6' => 'AWAY', '7' => 'CONNECT', '8' => 'PING', '9' => 'PONG',
			'<' => 'PASS', '>' => 'TIME', '@' => 'ADMIN', 'AA' => 'SETHOST', 'AC' => 'NACHAT',
			'AD' => 'SETIDENT', 'AE' => 'SETNAME', 'AF' => 'LAG', 'AG' => 'SDESC',
			'AI' => 'KNOCK', 'AJ' => 'CREDITS', 'AK' => 'LICENSE', 'AL' => 'CHGHOST',
			'AM' => 'RPING', 'AN' => 'RPONG', 'AO' => 'NETINFO', 'AP' => 'SENDUMODE',
			'AQ' => 'ADDMOTD', 'AR' => 'ADDOMOTD', 'AS' => 'SVSMOTD', 'AU' => 'SMO',
			'AV' => 'OPERMOTD', 'AW' => 'TSCTL', 'AX' => 'SAJOIN', 'AY' => 'SAPART',
			'AZ' => 'CHGIDENT', 'B' => 'NOTICE', 'BA' => 'SWHOIS', 'BB' => 'SVSO',
			'BC' => 'SVSFLINE', 'BD' => 'TKL', 'BE' => 'VHOST', 'BF' => 'BOTMOTD',
			'BH' => 'HTM', 'BI' => 'DCCDENY', 'BJ' => 'UNDCCDENY', 'BK' => 'CHGNAME',
			'BL' => 'SHUN', 'BP' => 'CYCLE', 'BQ' => 'MODULE', 'BR' => 'SVSNLINE',
			'BT' => 'SVSPART', 'BU' => 'SVSLUSERS', 'BV' => 'SVSSNO', 'BW' => 'SVS2SNO',
			'BX' => 'SVSJOIN', 'Bs' => 'SVSSILENCE', 'Bw' => 'SVSWATCH', 'C' => 'JOIN',
			'D' => 'PART', 'E' => 'LUSERS', 'ES' => 'EOS', 'F' => 'MOTD', 'G' => 'MODE',
			'H' => 'KICK', 'O' => 'REHASH', 'P' => 'RESTART', 'Q' => 'CLOSE',
			'Ss' => 'SENDSNO', 'T' => 'DNS', 'Tz' => 'TEMPSHUN', 'U' => 'SILENCE',
			'V' => 'AKILL', 'X' => 'UNKLINE', 'Y' => 'RAKILL', ']' => 'GLOBOPS',
			'^' => 'LOCOPS', '_' => 'PROTOCTL', '`' => 'WATCH', 'b' => 'TRACE',
			'c' => 'SQLINE', 'd' => 'UNSQLINE', 'e' => 'SVSNICK', 'f' => 'SVSNOOP',
			'h' => 'SVSKILL', 'n' => 'SVSMODE', 'o' => 'SAMODE', 'p' => 'CHATOPS',
			'r' => 'UNZLINE', 't' => 'RULES', 'u' => 'MAP', 'v' => 'SVS2MODE', 
			'w' => 'DALINFO', 'x' => 'ADMINCHAT', '|' => 'UMODE2', '~' => 'SJOIN');
		return (array_key_exists($token,$det) ? $det[$token] : $token);
	}
	static public function tokenizer($command) {
		$tokenizer = array('PRIVMSG' => '!', 'WHOIS' => '#', 'NICK' => '&', 'SERVER' => "'",
			'TOPIC' => ')', 'INVITE' => '*', 'VERSION' => '+', 'QUIT' => ',', 'SQUIT' => '-',
			'KILL' => '.', 'INFO' => '/', 'LINKS' => '0', 'STATS' => '2', 'HELP' => '4',
			'ERROR' => '5', 'AWAY' => '6', 'CONNECT' => '7', 'PING' => '8', 'PONG' => '9',
			'PASS' => '<', 'TIME' => '>', 'ADMIN' => '@', 'SETHOST' => 'AA', 'NACHAT' => 'AC',
			'SETIDENT' => 'AD', 'SETNAME' => 'AE', 'LAG' => 'AF', 'SDESC' => 'AG',
			'KNOCK' => 'AI', 'CREDITS' => 'AJ', 'LICENSE' => 'AK', 'CHGHOST' => 'AL',
			'RPING' => 'AM', 'RPONG' => 'AN', 'NETINFO' => 'AO', 'SENDUMODE' => 'AP',
			'ADDMOTD' => 'AQ', 'ADDOMOTD' => 'AR', 'SVSMOTD' => 'AS', 'SMO' => 'AU',
			'OPERMOTD' => 'AV', 'TSCTL' => 'AW', 'SAJOIN' => 'AX', 'SAPART' => 'AY',
			'CHGIDENT' => 'AZ', 'NOTICE' => 'B', 'SWHOIS' => 'BA', 'SVSO' => 'BB',
			'SVSFLINE' => 'BC', 'TKL' => 'BD', 'VHOST' => 'BE', 'BOTMOTD' => 'BF',
			'HTM' => 'BH', 'DCCDENY' => 'BI', 'UNDCCDENY' => 'BJ', 'CHGNAME' => 'BK',
			'SHUN' => 'BL', 'CYCLE' => 'BP', 'MODULE' => 'BQ', 'SVSNLINE' => 'BR',
			'SVSPART' => 'BT', 'SVSLUSERS' => 'BU', 'SVSSNO' => 'BV', 'SVS2SNO' => 'BW', 
			'SVSJOIN' => 'BX', 'SVSSILENCE' => 'Bs', 'SVSWATCH' => 'Bw', 'JOIN' => 'C',
			'PART' => 'D', 'LUSERS' => 'E', 'EOS' => 'ES', 'MOTD' => 'F', 'MODE' => 'G',
			'KICK' => 'H', 'REHASH' => 'O', 'RESTART' => 'P', 'CLOSE' => 'Q',
			'SENDSNO' => 'Ss', 'DNS' => 'T', 'TEMPSHUN' => 'Tz', 'SILENCE' => 'U',
			'AKILL' => 'V', 'UNKLINE' => 'X', 'RAKILL' => 'Y', 'GLOBOPS' => ']',
			'LOCOPS' => '^', 'PROTOCTL' => '_', 'WATCH' => '`', 'TRACE' => 'b',
			'SQLINE' => 'c', 'UNSQLINE' => 'd', 'SVSNICK' => 'e', 'SVSNOOP' => 'f',
			'SVSKILL' => 'h', 'SVSMODE' => 'n', 'SAMODE' => 'o', 'CHATOPS' => 'p',
			'UNZLINE' => 'r', 'RULES' => 't', 'MAP' => 'u', 'SVS2MODE' => 'v',
			'DALINFO' => 'w', 'ADMINCHAT' => 'x', 'UMODE2' => '|','SJOIN' => '~');
		return (array_key_exists($command,$tokenizer) ? $tokenizer[$command] : $command);
	}
}
?>